// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:io';

import 'package:front_end/src/api_prototype/experimental_flags.dart';
import 'package:testing/testing.dart'
    show Chain, ChainContext, Result, Step, TestDescription;
import "package:yaml/yaml.dart" show YamlMap, loadYamlNode;

import 'testing/folder_options.dart';
import 'utils/suite_utils.dart';
import 'parser_suite.dart'
    show ListenerStep, ParserTestListenerWithMessageFormatting;
import 'testing_utils.dart' show checkEnvironment;

void main([List<String> arguments = const []]) => internalMain(
  createContext,
  arguments: arguments,
  displayName: "parser equivalence suite",
  configurationPath: "../testing.json",
);

Future<Context> createContext(Chain suite, Map<String, String> environment) {
  const Set<String> knownEnvironmentKeys = {};
  checkEnvironment(environment, knownEnvironmentKeys);

  return new Future.value(new Context(suite.root, suite.name, environment));
}

class Context extends ChainContext {
  final SuiteFolderOptions folderOptions;
  final String suiteName;
  final Map<ExperimentalFlag, bool> forcedExperimentalFlags;

  Context(Uri baseUri, this.suiteName, Map<String, String> environment)
    : folderOptions = new SuiteFolderOptions(baseUri),
      forcedExperimentalFlags =
          SuiteFolderOptions.computeForcedExperimentalFlags(environment);

  @override
  final List<Step> steps = const <Step>[const ListenerCompareStep()];
}

class ListenerCompareStep
    extends Step<TestDescription, TestDescription, Context> {
  const ListenerCompareStep();

  @override
  String get name => "listenerCompare";

  @override
  Future<Result<TestDescription>> run(
    TestDescription description,
    Context context,
  ) {
    Map<ExperimentalFlag, bool> experimentalFlags = description
        .computeExplicitExperimentalFlags(context);
    Uri uri = description.uri;
    String contents = new File.fromUri(uri).readAsStringSync();
    YamlMap yaml = loadYamlNode(contents, sourceUrl: uri) as YamlMap;
    List<Uri> files = (yaml["files"] as List)
        .map((s) => uri.resolve(s))
        .toList();
    Set<String> filters = new Set<String>.from(yaml["filters"] ?? []);
    Set<String> ignored = new Set<String>.from(yaml["ignored"] ?? []);

    ParserTestListenerWithMessageFormatting? parserTestListenerFirst =
        ListenerStep.doListenerParsing(
          files[0],
          context.suiteName,
          experimentalFlags,
          description.shortName,
        );
    if (parserTestListenerFirst == null) {
      return Future.value(crash(description, StackTrace.current));
    }

    for (int i = 1; i < files.length; i++) {
      ParserTestListenerWithMessageFormatting? parserTestListener =
          ListenerStep.doListenerParsing(
            files[i],
            context.suiteName,
            experimentalFlags,
            description.shortName,
          );
      if (parserTestListener == null) {
        return Future.value(crash(description, StackTrace.current));
      }
      String? compareResult = compare(
        parserTestListenerFirst,
        parserTestListener,
        filters,
        ignored,
      );
      if (compareResult != null) {
        return Future.value(
          fail(description, compareResult, StackTrace.current),
        );
      }
    }

    return new Future.value(new Result<TestDescription>.pass(description));
  }

  String? compare(
    ParserTestListenerWithMessageFormatting a,
    ParserTestListenerWithMessageFormatting b,
    Set<String> filters,
    Set<String> ignored,
  ) {
    List<String> aLines = a.sb.toString().split("\n");
    List<String> bLines = b.sb.toString().split("\n");

    bool doRemoveListenerArguments = filters.contains(
      "ignoreListenerArguments",
    );

    int aIndex = 0;
    int bIndex = 0;
    while (aIndex < aLines.length && bIndex < bLines.length) {
      String aLine = aLines[aIndex];
      String bLine = bLines[bIndex];
      if (doRemoveListenerArguments) {
        aLine = removeListenerArguments(aLine);
        bLine = removeListenerArguments(bLine);
      }
      bool anyIgnored = false;
      if (ignored.contains(aLine.trim())) {
        anyIgnored = true;
        aIndex++;
      }
      if (ignored.contains(bLine.trim())) {
        anyIgnored = true;
        bIndex++;
      }
      if (anyIgnored) continue;
      if (aLine.trim() != bLine.trim()) {
        return "Disagreement: '${aLine}' vs '${bLine}'";
      }
      aIndex++;
      bIndex++;
    }

    // Any trailing lines?
    while (aIndex < aLines.length) {
      String aLine = aLines[aIndex];
      if (doRemoveListenerArguments) {
        aLine = removeListenerArguments(aLine);
      }
      if (ignored.contains(aLine.trim())) {
        aIndex++;
        continue;
      }
      return "Unmatched line at end: '$aLine'";
    }
    while (bIndex < bLines.length) {
      String bLine = bLines[bIndex];
      if (doRemoveListenerArguments) {
        bLine = removeListenerArguments(bLine);
      }
      if (ignored.contains(bLine.trim())) {
        bIndex++;
        continue;
      }
      return "Unmatched line at end: '$bLine'";
    }
    return null;
  }

  String removeListenerArguments(String s) {
    int index = s.indexOf("(");
    if (index < 0) return s;
    return s.substring(0, index);
  }
}

extension on TestDescription {
  FolderOptions computeFolderOptions(Context context) {
    return context.folderOptions.computeFolderOptions(this);
  }

  Map<ExperimentalFlag, bool> computeExplicitExperimentalFlags(
    Context context,
  ) {
    return computeFolderOptions(
      context,
    ).computeExplicitExperimentalFlags(context.forcedExperimentalFlags);
  }
}
